package com.jiang.books.service.jpa;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Set;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import javax.validation.Validator;
import javax.validation.ConstraintViolation;

import org.apache.log4j.Logger;
import org.hibernate.envers.AuditReader;
import org.hibernate.envers.AuditReaderFactory;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.google.common.collect.Lists;
import com.jiang.books.common.utils.annotation.AdditionalLogging;
import com.jiang.books.domain.Book;
import com.jiang.books.domain.BookSearchCriteria;
import com.jiang.books.domain.metamodel.Book_;
import com.jiang.books.repository.BookRepository;
import com.jiang.books.service.BookService;

@Service("jpaBookService")
@Repository
@Transactional
public class BookServiceImpl implements BookService {

	private static final Logger log = Logger.getRootLogger();
	
	@PersistenceContext
	private EntityManager entityManager;

	@Autowired
	private BookRepository repository;
	
	@Autowired
	private Validator validator;

	@Override
	@AdditionalLogging
	@Transactional(readOnly=true)
	public List<Book> findAll() {
		log.info("Book#Find All Books");
		/*
		List<Book> books = entityManager.createNamedQuery("Book.findAll", Book.class).getResultList();
		return books;
		*/
		return Lists.newArrayList(repository.findAll());
	}

	@Override
    @Transactional(readOnly=true)
    public Book findById(BigInteger id) {
    	log.info("Book#Find Book by id: " + id);
    	/*
        TypedQuery<Book> query = entityManager.createNamedQuery("Book.findById", Book.class);
        query.setParameter("id", id);
        return query.getSingleResult();
        */
        return repository.findOne(id);
    }
    
    @Override
    @Transactional(readOnly=true)
    public Book findByIdAndRevision(BigInteger id, int revision) {
    	log.info("Book#Find Book by id: " + id + " and Revision: " + revision);
        AuditReader auditReader = AuditReaderFactory.get(entityManager);
        return auditReader.find(Book.class, id, revision);
    }
    
    @Override
	@Transactional(readOnly=true)
	public List<Book> findAuditById(BigInteger id) {
		log.info("Book#Find Book Audit by id: " + id);
		AuditReader auditReader = AuditReaderFactory.get(entityManager);
		List<Number> revisions = auditReader.getRevisions(Book.class, id);
		
		revisions = Lists.reverse(revisions);

		List<Book> books = new ArrayList<Book>();
		Book book;
		for (Number revision: revisions) {
			book = auditReader.find(Book.class, id, revision);
			books.add(book);
		}
		return books;
	}

	@Override
	public Book save(Book book) {
		/*
		if (book.getId() == null) { // Insert Book
			log.info("Book#Inserting new book");
			entityManager.persist(book);
		} else {                    // Update Book
			entityManager.merge(book);
			log.info("Book#Updating existing book");
		}
		log.info("Book#Book saved with id: " + book.getId());
		return book;
		*/
		log.info("Book#Save Book with id: " + (!book.isNew() ? book.getId() : "not yet generated"));
		return repository.save(book);
	}

	@Override
	public void delete(Book book) {
		/*
        Book mergedBook = entityManager.merge(book);
        entityManager.remove(mergedBook);
		log.info("Book#Book with id: " + book.getId() + " deleted successfully");
		*/
		repository.delete(book);
		log.info("Book#Book with id: " + book.getId() + " deleted successfully");
	}
	
	@Override
    @Transactional(readOnly=true)
    public List<Book> findByAuthorId(BigInteger id) {
    	log.info("Book#Find Book by Author Id: " + id);
        TypedQuery<Book> query = entityManager.createNamedQuery("Book.findByAuthorId", Book.class);
        query.setParameter("id", id);
        return query.getResultList();
    }

    @Override
    @Transactional(readOnly=true)
    public List<Book> findByAwardId(BigInteger id) {
    	log.info("Book#Find Book by Award: " + id);
        TypedQuery<Book> query = entityManager.createNamedQuery("Book.findByAwardId", Book.class);
        query.setParameter("id", id);
        return query.getResultList();
    }

    
    // CrudRepository
    @Override
    public Book findByTitle(String title) {
    	log.info("Book#Find Book by title: " + title);
    	return repository.findByTitle(title);
    }

	@Override
	@Transactional(readOnly=true)
	public Page<Book> findAllByPage(Pageable pageable) {
		return repository.findAll(pageable);
	}
    
    
    @Override
    @Transactional(readOnly=true)
    public List<Book> findByNativeQuery(String query) {
    	log.info("Book#Find Book by native query: " + query);
        return entityManager.createNativeQuery(query, "bookResult").getResultList();
    }
    
    @Override
	@Transactional(readOnly=true)
	public List<Book> findByCriteriaQuery(String title) {
		log.info("Book#Find Book using CriteriaQuery by title: " + title);
		
		CriteriaBuilder cb = entityManager.getCriteriaBuilder();
		CriteriaQuery<Book> criteriaQuery = cb.createQuery(Book.class);
		Root<Book> bookRoot = criteriaQuery.from(Book.class);
		bookRoot.fetch(Book_.awards, JoinType.LEFT);

		criteriaQuery.select(bookRoot).distinct(true);
		
		Predicate criteria = cb.conjunction();

		// Title
		if (title != null) {
			Predicate p = cb.equal(bookRoot.get(Book_.title), title);
			criteria = cb.and(criteria, p);
		}		
		
		criteriaQuery.where(criteria);
		List<Book> result = entityManager.createQuery(criteriaQuery).getResultList();
		return result;
	}
	
	@Override
	@Transactional(readOnly=true)
	public Page<Book> findByCriteria(BookSearchCriteria searchCriteria, Pageable pageable) {
		log.info("Book#Find Book using SearchCriteria: " + searchCriteria);
		String bookTitle = searchCriteria.getBookTitle();
		String bookType = searchCriteria.getBookType();
		Date fromPublishedDate = searchCriteria.getFromPublishedDate();
		Date toPublishedDate = searchCriteria.getToPublishedDate();
		BigInteger authorId = searchCriteria.getAuthorId();
		BigInteger awardId = searchCriteria.getAwardId();
		return repository.findByCriteria(bookTitle, bookType, fromPublishedDate, toPublishedDate, authorId, awardId, pageable);
	}
	
	public Set<ConstraintViolation<Book>> validateBook(Book book) {
		return validator.validate(book);
	}
	
	//test
    @Transactional(readOnly=true)
    public List<Book> findAllWithAwards() {
        List<Book> books = entityManager.createNamedQuery("Book.findAllWithAwards", Book.class).getResultList();
        return books;
    }
}